﻿/***************************************************COPYRIGHT***************************************
* Copyright (c) 2008, Whiteboard-IT
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
* the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following 
 * disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the distribution.
*
* Neither the name of Whiteboard-IT, LLC nor the names of its contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
*****************************************************************************************************************/
/*
* User: Nathan
* Date: 4/27/2008
* Time: 9:02 PM
* 
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using Boo.Lang;
using Castle.Core.Logging;
using Mite.Configuration;

namespace Mite
{
	/// <summary>
	/// Description of MyClass.
	/// </summary>
	public abstract partial class MigrationBase
	{
		protected MigrationBase(MiteEnvironment environment)
			: this(environment, IoC.Resolve<IAdapterFactory>().Create(environment))
		{
		}

		protected MigrationBase(MiteEnvironment environment, IAdapter adapter)
		{
			_environment = environment;
			_adapter = adapter;
			_messages = new List<MigrationMessage>();
			Logger = NullLogger.Instance;
		}

		protected IDbTransaction Transaction { get; set; }

		public ILogger Logger { get; set; }

		public abstract uint Version { get; }

		public bool IsFatal
		{
			get { return TransformationMessages.Where(mig => mig.IsFatal) != null; }
		}

		public IEnumerable<MigrationMessage> TransformationMessages
		{
			get
			{
				var result = new List<MigrationMessage>();
				return result.ToArray();
			}
		}

		/// <summary>
		/// Execute the migration.
		/// </summary>
		/// <param name="direction">The direction to run the migration.</param>
		public virtual void Run(MigrationDirection direction)
		{
			ClearState();
			Database db = GetCachedDatabase();
			int currentVersion = GetCurrentVersion(db);

			// Down migrations expect to run on the same version as their version property.  
			// Up migrations expect to run on the version lower than their version property.
			uint expectedVersion = direction == MigrationDirection.Down ? Version : Version - 1;

			if (expectedVersion != currentVersion)
				throw new MigrationVersionException(Version, expectedVersion);

			using (IDbConnection conn =
				CreateConnection())
			{
				conn.Open();
				Transaction = conn.BeginTransaction();

				try
				{
					if (currentVersion == 0 &&
						db.Tables.DoesNotContain(tbl => tbl.Name.Equals(Adapter.MIGRATION_SCHEMA_TABLE_NAME)))
					{
						IDbCommand[] cmd = _adapter.CreateSchemaTable(conn, db);
						AddToTransaction(cmd);
					}

					if (direction == MigrationDirection.Up)
					{
						Up();
						SetSchemaVersion(currentVersion + 1);
					}
					else
					{
						Down();
						SetSchemaVersion(currentVersion - 1);
					}

					Transaction.Commit();
					conn.Close();
				}
				catch (Exception ex)
				{
					if (Logger != null)
						Logger.Error(ex.Message, ex);
					if (Transaction != null)
						Transaction.Rollback();
					throw;
				}
				finally
				{
					if (Transaction != null)
						Transaction.Dispose();
				}
			}
		}

		protected abstract void Up();

		protected abstract void Down();

		/// <summary>
		/// Create table <paramref name="tableName"/>.
		/// </summary>
		/// <param name="tableName">Name of table to create.</param>
		/// <param name="columns">Columns in the new table.</param>
		protected void CreateTable(string tableName, params Column[] columns)
		{
			Logger.DebugFormat("Entering CreateTable: {0}", tableName);

			Database db = GetCachedDatabase();
			var tbl = new Table(tableName, columns);
			db.AddTable(tbl);

			IDbCommand cmd = _adapter.CreateTable(Transaction.Connection, tbl);

			Logger.DebugFormat("Create Table Command: {0}", cmd.CommandText);

			AddToTransaction(cmd);

			Logger.DebugFormat("Leaving CreateTable: {0}", tableName);
		}

		/// <summary>
		/// Drop table <paramref name="tableName"/>.
		/// </summary>
		/// <param name="tableName">Name of the table to drop.</param>
		protected void DropTable(string tableName)
		{
			Database db = GetCachedDatabase();

			Table table = db.Tables.SingleOrDefault(tbl => tbl.Name.Equals(tableName));
			if (table == null)
				throw new ArgumentException("Table not found: " + tableName);

			AddToTransaction(_adapter.DropTable(Transaction.Connection, table));
		}

		/// <summary>
		/// Rename table <paramref name="oldName"/>.
		/// </summary>
		/// <param name="oldName">Name of the table to be renamed.</param>
		/// <param name="newName">New name of the table.</param>
		protected void RenameTable(string oldName, string newName)
		{
			Guard.StringArgumentNotNullOrEmpty("oldName", oldName);
			Guard.StringArgumentNotNullOrEmpty("newName", newName);

			Database db = GetCachedDatabase();

			Table table = db.Tables.SingleOrDefault(tbl => tbl.Name.Equals(oldName));
			if (table == null)
				throw new ArgumentException("Table not found: " + oldName);

			IDbCommand cmd = _adapter.RenameTable(Transaction.Connection, table, newName);
			AddToTransaction(cmd);

			table.Rename(newName);
		}

		/// <summary>
		/// Adds <paramref name="column"/> to <paramref name="table"/>.
		/// </summary>
		/// <param name="table">Name of table to add the column to.</param>
		/// <param name="column">Column to add.</param>
		protected void AddColumn(string table, Column column)
		{
			Database db = GetCachedDatabase();
			Table tbl = db.Tables.SingleOrDefault(t => t.Name.Equals(table));

			if (tbl == null)
				throw new ArgumentException(string.Format("Table not found: {0}", table));

			if (tbl.Columns.Contains(col => col.Name.Equals(column)))
				throw new ArgumentException(string.Format("Table already contains column {0}", column.Name));

			IDbCommand cmd = _adapter.AddColumnToTable(Transaction.Connection, new Table(table), column);
			AddToTransaction(cmd);
		}

		/// <summary>
		/// Remove column named <paramref name="columnName"/> from table named <paramref name="tableName"/>.
		/// </summary>
		/// <param name="tableName">Name of the table that the column belongs to.</param>
		/// <param name="columnName">Name of the column to remove.</param>
		protected void RemoveColumn(string tableName, string columnName)
		{
			Database db = GetCachedDatabase();

			IDbCommand cmd = _adapter.DropColumns(Transaction.Connection, tableName, columnName);
			AddToTransaction(cmd);
		}

		/// <summary>
		/// Rename column named <paramref name="oldColumnName"/> in <paramref name="tableName"/> to
		/// <paramref name="newColumnName"/>.
		/// </summary>
		/// <param name="tableName">Table the column is in.</param>
		/// <param name="oldColumnName">The current (old) name of the column to rename.</param>
		/// <param name="newColumnName">Name to rename the column to.</param>
		protected void RenameColumn(string tableName, string oldColumnName, string newColumnName)
		{
			Database db = GetCachedDatabase();
			Table tbl = db.Tables.Single(t => t.Name.Equals(tableName));
			Column column = tbl.Columns.Single(c => c.Name.Equals(oldColumnName));

			throw new NotImplementedException();
		}

		/// <summary>
		/// Adds a foreign key to <paramref name="table"/> referencing 
		/// <paramref name="referenceTable"/>.<paramref name="referenceColumn"/>.
		/// </summary>
		/// <remarks>
		/// The name of the foreign key will be conventions based.
		/// </remarks>
		/// <param name="table">Table in which to add the new foreign key.</param>
		/// <param name="referenceTable">Table that is referenced by the new foreign key.</param>
		/// <param name="referenceColumn">Column in the reference table that is referenced by the foreign key.</param>
		protected void AddForeignKey(string table, string referenceTable)
		{
			Guard.StringArgumentNotNullOrEmpty("table", table);
			Guard.StringArgumentNotNullOrEmpty("referenceTable", referenceTable);

			Table tbl = GetTable(table);
			Table refTable = GetTable(referenceTable);
			if (refTable == null)
				throw new ArgumentException(string.Format("Table not found: {0}", referenceTable));

			ForeignKey fk = tbl.CreateForeignKeyTo(refTable);

			List<IDbCommand> cmds = new List<IDbCommand>();
			foreach (var column in fk.Columns)
				cmds.Add(_adapter.AddColumnToTable(Transaction.Connection, tbl, column));

			throw new NotImplementedException();
			//cmds.Add(_adapter.AddForeignKey(Transaction.Connection, fk));

			AddToTransaction(cmds.ToArray());
		}

		/// <summary>
		/// Get a db command associated with the current connection.
		/// </summary>
		protected IDbCommand GetCommand()
		{
			return Transaction.Connection.CreateCommand();
		}

		/// <summary>
		/// Execute a db command as a part of the current migration.
		/// </summary>
		/// <param name="cmd"></param>
		protected void ExecuteCommand(IDbCommand cmd)
		{
			AddToTransaction(cmd);
		}

		private Table GetTable(string table)
		{
			Database db = GetCachedDatabase();
			return db.Tables.SingleOrDefault(tbl => tbl.Name.Equals(table));
		}

		/// <param name="table">Name of the table that contains the column to change.</param>
		/// <param name="column">Name of the column to change in the format of &quot;table.column&quot;</param>
		/// <param name="type">Type the column is to be changed to.</param>
		/// <param name="options">
		/// Hash of options.  Recognized values include 
		/// <list>
		///	<item><c>default</c></item>
		///	<item><c>limit</c></item>
		///	<item><c>null</c></item>
		/// </list>
		/// </param>
		protected void ChangeColumn(string table, string column, DbType type, Hash options)
		{
			Guard.StringArgumentNotNullOrEmpty("table", table);
			Guard.StringArgumentNotNullOrEmpty("column", column);

			Database db = GetCachedDatabase();
			Table tbl = db.Tables.SingleOrDefault(t => t.Name.Equals(table));
			if (tbl == null)
				throw new ArgumentException(string.Format("Table not found: {0}", table));

			Column col = tbl.Columns.SingleOrDefault(c => c.Name.Equals(column));
			if (col == null)
				throw new ArgumentException(string.Format("Column not found: {0}", column));

			var altered = col.Clone() as Column;
			altered.DataType = type;

			IDbCommand cmd = _adapter.AlterColumn(Transaction.Connection, col, altered);
			AddToTransaction(cmd);
		}

		/// <summary>
		/// <seealso cref="ChangeColumn(string,string,DbType,Hash)"/>
		/// </summary>
		protected void ChangeColumn(string table, string column, DbType type)
		{
			ChangeColumn(table, column, type, new Hash());
		}

		private IDbConnection CreateConnection()
		{
			return _adapter.CreateConnection(_environment.Host, _environment.Database,
											 _environment.Username,
											 _environment.Password,
											 _environment.Options);
		}

		private void AddToTransaction(params IDbCommand[] cmds)
		{
			foreach (IDbCommand cmd in cmds)
			{
				Debug.Assert(cmd.Connection == Transaction.Connection);

				cmd.Transaction = Transaction;
				cmd.ExecuteNonQuery();
			}
		}

		private Database GetCachedDatabase()
		{
			if (database == null)
			{
				using (IDbConnection conn = CreateConnection())
				{
					conn.Open();

					database = _adapter.GetDatabase(conn, _environment.Database);

					conn.Close();
				}
			}
			return database;
		}

		private int GetCurrentVersion(Database database)
		{
			using (IDbConnection conn = CreateConnection())
			{
				conn.Open();
				return _adapter.GetSchemaVersion(conn, database);
			}
		}

		private void SetSchemaVersion(int i)
		{
			IDbCommand cmds = _adapter.SetSchemaVersion(Transaction.Connection, i);
			AddToTransaction(cmds);
		}

		/// <summary>
		/// Clears the state of the migration
		/// </summary>
		private void ClearState()
		{
			database = null;
		}

		private readonly IAdapter _adapter;

		protected Column ForeignKey(string referenceTable, Hash options)
		{
			Table refTable = GetTable(referenceTable);
			Column refColumn = refTable.Columns.Single(col => col.IsPrimaryKey);

			string defaultName = refTable.Name + refColumn.Name;

			Column fk = new Column(defaultName, refColumn.DataType, options);

			fk.Reference(refColumn);

			return fk;
		}

		protected Column ForeignKey(string referenceTable)
		{
			return ForeignKey(referenceTable, new Hash());
		}

		protected Column ForeignKey(string referenceTable, string referenceColumn, Hash options)
		{
			Table refTable = GetTable(referenceTable);
			Column refColumn = refTable.Columns.SingleOrDefault(col => col.Name.Equals(referenceColumn));

			if (refColumn == null)
				throw new ArgumentException("Column not found: {0}", referenceColumn);

			Column fk = new Column
						{
							Name = refTable.Name + refColumn.Name,
							DataType = refColumn.DataType,
							IsNullable = false,
							Length = refColumn.Length,
							Precision = refColumn.Precision,
							Scale = refColumn.Scale
						};

			fk.Reference(refColumn);

			return fk;
		}

		private readonly MiteEnvironment _environment;
		private readonly List<MigrationMessage> _messages;
		private Database database;
	}

	public enum MigrationDirection
	{
		Up,
		Down
	}
}